package org.groovymud.shell.security;

import java.io.IOException;
import java.security.Principal;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.ConfirmationCallback;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.FailedLoginException;
import javax.security.auth.login.LoginException;
import javax.security.auth.spi.LoginModule;

import org.apache.log4j.Logger;
import org.groovymud.object.alive.Player;

public class MudLoginModule implements LoginModule {

    private static final String GUEST_USER = "guest";
    private static final String PLAYER_PRINCIPAL = "player";
    // initial state
    private Subject subject;
    private CallbackHandler callbackHandler;
    private Map sharedState;
    private Map options;

    // configurable option
    private boolean debug = false;

    // the authentication status
    private boolean succeeded = false;
    private boolean commitSucceeded = false;

    // username and password
    private String username;
    private char[] password;

    Player player = null;
    Player dupe = null;

    private Set<Principal> playerPrincipals;

    private final static Logger logger = Logger.getLogger(MudLoginModule.class);

    public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState, Map<String, ?> options) {
        this.subject = subject;
        this.callbackHandler = callbackHandler;
        this.sharedState = sharedState;
        this.options = options;
        playerPrincipals = new HashSet<Principal>();
        getPlayerPrincipals().add(createPrincipal(GUEST_USER));
        // initialize any configured options
        debug = "true".equalsIgnoreCase((String) options.get("debug"));

    }

    protected MudPrincipal createPrincipal(String principal) {
        return new MudPrincipal(principal);
    }

    public boolean login() throws LoginException {
        try {
            NameCallback nameCallback = createNameCallback("username: ");
            Callback[] cb = createCallbackArray(nameCallback);

            getCallbackHandler().handle(cb);
            setUsername(nameCallback.getName());
            retrieveExistingPlayer(); // populate dupe player if there is one
            doLoadPlayerCallback(); // load the player
            if (!getUsername().equals(GUEST_USER)) {
                getPlayerPrincipals().add(createPrincipal(PLAYER_PRINCIPAL));
                checkPasswordValid(player);
                confirmDestIfDupeExisted();

            } else {
                // always create guests
                doCreatePlayerCallback();
            }
            // successful log in
            setSucceeded(true);
            return true;
        } catch (IOException e) {
            logger.error(e, e);
            throw new LoginException(e.getMessage());
        } catch (UnsupportedCallbackException e) {
            logger.error(e, e);
            throw new LoginException(e.getMessage());
        }
    }

    protected Callback[] createCallbackArray(Callback... callback) {
        return callback;
    }

    protected NameCallback createNameCallback(String prompt) {
        return new NameCallback(prompt);
    }

    protected void doLoadPlayerCallback() throws IOException, UnsupportedCallbackException {
        Callback[] cb;
        ActionCallback loadCallback = createActionCallback(username, ActionCallback.LOAD_ACTION);
        cb = createCallbackArray(loadCallback);
        getCallbackHandler().handle(cb);
        setPlayer((Player) loadCallback.getMudObject());
    }

    protected ActionCallback createActionCallback(String username, String action) {
        return new ActionCallback(username, action);
    }

    protected void confirmDestIfDupeExisted() throws IOException, UnsupportedCallbackException, FailedLoginException {
        Callback[] cb;
        Player dupe = getDupe();
        if (dupe != null) {
            // we need to dest this player if they say yes
            ConfirmationCallback alreadyPlaying = new ConfirmationCallback("You are already playing. Throw the other copy out?", ConfirmationCallback.INFORMATION, ConfirmationCallback.YES_NO_OPTION, ConfirmationCallback.NO);
            cb = createCallbackArray(alreadyPlaying);
            getCallbackHandler().handle(cb);
            if (alreadyPlaying.getSelectedIndex() == ConfirmationCallback.YES) {
                dupe.dest(true);
            } else {
                throw new FailedLoginException("cannot log in, already logged in!");
            }
        }
    }

    protected void checkPasswordValid(Player player) throws IOException, UnsupportedCallbackException, FailedLoginException {
        Callback[] cb;
        PasswordCallback passwordCallback = null;
        PasswordCallback checkPassword = null;

        if (player == null) {
            ConfirmationCallback confirm = new ConfirmationCallback("Your name is not in our annuls. Did you type it correctly?", ConfirmationCallback.INFORMATION, ConfirmationCallback.YES_NO_OPTION, ConfirmationCallback.YES);
            checkPassword = createPasswordCallback("again:", false);
            passwordCallback = createPasswordCallback("enter a password:", false);

            cb = createCallbackArray(confirm, passwordCallback, checkPassword);
        } else {
            passwordCallback = createPasswordCallback("password: ", false);
            cb = createCallbackArray(passwordCallback);
        }
        getCallbackHandler().handle(cb);
        setPassword(passwordCallback.getPassword()); // set the password
        boolean passwordValid = false;
        if (checkPassword != null) {
            passwordValid = checkSamePassword(getPassword(), checkPassword.getPassword());
        } else {
            passwordValid = checkSamePassword(getPassword(), player.getPassword().toCharArray());
        }
        if (!passwordValid) {
            throw new FailedLoginException("passwords do not match.");
        }
        if (checkPassword != null) {
            doCreatePlayerCallback();
        }
    }

    protected PasswordCallback createPasswordCallback(String prompt, boolean echo) {
        return new PasswordCallback(prompt, echo);
    }

    protected void doCreatePlayerCallback() throws IOException, UnsupportedCallbackException {
        Callback[] cb;
        ActionCallback createCallback;
        createCallback = new ActionCallback(getUsername(), ActionCallback.CREATE_ACTION);
        cb = createCallbackArray(createCallback);
        getCallbackHandler().handle(cb);
        setPlayer((Player) createCallback.getMudObject());
    }

    protected void retrieveExistingPlayer() throws IOException, UnsupportedCallbackException, FailedLoginException {
        Callback[] cb;
        ActionCallback checkIfLoggedIn = createActionCallback(getUsername(), ActionCallback.EXISTS_ACTION);
        cb = createCallbackArray(checkIfLoggedIn);
        getCallbackHandler().handle(cb);
        setDupe((Player) checkIfLoggedIn.getMudObject());
    }

    protected boolean checkSamePassword(char[] password, char[] passwordOther) throws FailedLoginException {
        boolean passwordValid = true;
        if (passwordOther.length == 0) {
            throw new FailedLoginException("password cannot be 0 length");
        }
        for (int x = 0; x < passwordOther.length; x++) {
            if (passwordOther[x] != password[x]) {
                passwordValid = false;
            }
        }
        return passwordValid;
    }

    public boolean abort() throws LoginException {
        if (!hasSucceeded()) {
            return false;
        }
        if (hasSucceeded() && hasCommitSucceeded()) {
            logout();
        } else {
            setSucceeded(false);
        }
        return true;
    }

    public boolean logout() throws LoginException {
        getSubject().getPrincipals().removeAll(getPlayerPrincipals());
        cleanup();
        player = null;
        playerPrincipals = null;
        setSucceeded(setCommitSucceeded(false));
        return true;
    }

    public boolean commit() throws LoginException {
        if (!hasSucceeded()) {
            cleanup();
            return false;
        }
        Player player = getPlayer();
        if (player != null) {
            getPlayerPrincipals().addAll(player.getSubject().getPrincipals());
            logger.info("player principals:");
            for(Principal p : getPlayerPrincipals()){
                logger.info(p.getName());
            }
            getSubject().getPrincipals().addAll(getPlayerPrincipals());
            player.setSubject(getSubject()); // update the players principals
            player.setPassword(new String(getPassword()));
            cleanup();
            setCommitSucceeded(true);

        } else {
            throw new LoginException("player was null");
        }
        return true;
    }

    protected void cleanup() {
        username = null;
        password = null;
        dupe = null;
    }

    public Player getPlayer() {
        return player;
    }

    public void setPlayer(Player player) {
        this.player = player;
    }

    public CallbackHandler getCallbackHandler() {
        return callbackHandler;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public char[] getPassword() {
        return password;
    }

    public void setPassword(char[] password) {
        this.password = password;
    }

    public Subject getSubject() {
        return subject;
    }

    public void setSubject(Subject subject) {
        this.subject = subject;
    }

    public Player getDupe() {
        return dupe;
    }

    public void setDupe(Player dupe) {
        this.dupe = dupe;
    }

    public void setCallbackHandler(CallbackHandler callbackHandler) {
        this.callbackHandler = callbackHandler;
    }

    public Set<Principal> getPlayerPrincipals() {
        return playerPrincipals;
    }

    public void setSucceeded(boolean succeeded) {
        this.succeeded = succeeded;
    }

    public boolean hasSucceeded() {
        return succeeded;
    }

    public boolean setCommitSucceeded(boolean commitSucceeded) {
        this.commitSucceeded = commitSucceeded;
        return commitSucceeded;
    }

    public boolean hasCommitSucceeded() {
        return commitSucceeded;
    }
}
